// Copyright 2018 The go-ego Project Developers.
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

// package tt is simple and colorful test tools

package tt

import (
	"fmt"
	"runtime"
	"strings"
	"testing"
	"unicode"
	"unicode/utf8"
)

// TestingT is an interface wrapper around *testing.T
type TestingT interface {
	Errorf(format string, args ...interface{})
}

// Blue returns a blue string
func Blue(message string) string {
	return fmt.Sprintf("\x1b[34m%s\x1b[0m", message)
}

// Red returns a red string
func Red(message string) string {
	return fmt.Sprintf("\x1b[31m%s\x1b[0m", message)
}

// Yellow returns a yellow string
func Yellow(message string) string {
	return fmt.Sprintf("\x1b[33m%s\x1b[0m", message)
}

//Bold returns a blod string
func Bold(message string) string {
	return fmt.Sprintf("\x1b[1m%s\x1b[21m", message)
}

// RedBold returns a red bold string
func RedBold(message string) string {
	return fmt.Sprintf("\x1b[31m%s\x1b[0m", Bold(message))
}

func isTest(name, prefix string) bool {
	if !strings.HasPrefix(name, prefix) {
		return false
	}
	if len(name) == len(prefix) { // "Test" is ok
		return true
	}
	rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
	return !unicode.IsLower(rune)
}

// CallerInfo returns an array of strings containing the file and line number
// of each stack frame leading from the current test to the assert call that
// failed.
func CallerInfo() []string {

	pc := uintptr(0)
	file := ""
	line := 0
	ok := false
	name := ""

	callers := []string{}
	for i := 0; ; i++ {
		pc, file, line, ok = runtime.Caller(i)
		if !ok {
			// The breaks below failed to terminate the loop, and we ran off the
			// end of the call stack.
			break
		}

		// This is a huge edge case, but it will panic if this is the case, see #180
		if file == "<autogenerated>" {
			break
		}

		f := runtime.FuncForPC(pc)
		if f == nil {
			break
		}
		name = f.Name()

		// testing.tRunner is the standard library function that calls
		// tests. Subtests are called directly by tRunner, without going through
		// the Test/Benchmark/Example function that contains the t.Run calls, so
		// with subtests we should break when we hit tRunner, without adding it
		// to the list of callers.
		if name == "testing.tRunner" {
			break
		}

		parts := strings.Split(file, "/")
		file = parts[len(parts)-1]
		if len(parts) > 1 {
			dir := parts[len(parts)-2]
			if (dir != "assert" && dir != "mock" && dir != "require") ||
				file == "mock_test.go" {
				callers = append(callers, fmt.Sprintf("%s:%d", file, line))
			}
		}

		// Drop the package
		segments := strings.Split(name, ".")
		name = segments[len(segments)-1]
		if isTest(name, "Test") ||
			isTest(name, "Benchmark") ||
			isTest(name, "Example") {
			break
		}
	}

	return callers
}

// BM func Benchmark1(b *testing.B, fn func())
func BM(b *testing.B, fn func()) {
	for i := 0; i < b.N; i++ {
		fn()
	}
}

// FmtErr return error string
func FmtErr(call int) string {
	err := RedBold("\n Error Trace:		" + CallerInfo()[call] + ",")
	err += Yellow("\n Error:		Not equal; \n ")
	err += Blue("expected:	'%s',\n ") + Red("but got:	'%s' \n\n")
	return err
}

// Equal asserts that two objects are equal.
//
//    tt.Equal(t *testing.T, 1, 1)
//
func Equal(t TestingT, expect, actual interface{}, args ...int) bool {
	call := 4
	if len(args) > 0 {
		call = args[0]
	}

	expectStr := fmt.Sprint(expect)
	return Expect(t, expectStr, actual, call)
}

// Expect asserts that string and objects are equal.
//
//    tt.Expect(t *testing.T, "1", 1)
//
func Expect(t TestingT, expect string, actual interface{}, args ...int) bool {
	call := 3
	if len(args) > 0 {
		call = args[0]
	}

	actualStr := fmt.Sprint(actual)
	if expect != actualStr {
		err := FmtErr(call)
		t.Errorf(err, expect, actualStr)

		return false
	}

	return true
}

// Nil asserts that nil and objects are equal.
func Nil(t TestingT, actual interface{}, args ...int) bool {
	call := 4
	if len(args) > 0 {
		call = args[0]
	}

	return Expect(t, "<nil>", actual, call)
}

// Empty asserts that empty and objects are equal.
func Empty(t TestingT, actual interface{}, args ...int) bool {
	call := 4
	if len(args) > 0 {
		call = args[0]
	}

	return Expect(t, "", actual, call)
}

// Bool asserts that true and objects are equal.
func Bool(t TestingT, actual interface{}, args ...int) bool {
	call := 4
	if len(args) > 0 {
		call = args[0]
	}

	return Expect(t, "true", actual, call)
}

// True asserts that true and objects are equal.
func True(t TestingT, actual interface{}, args ...int) bool {
	call := 4
	if len(args) > 0 {
		call = args[0]
	}

	return Expect(t, "true", actual, call)
}

// False asserts that flase and objects are equal.
func False(t TestingT, actual interface{}, args ...int) bool {
	call := 4
	if len(args) > 0 {
		call = args[0]
	}

	return Expect(t, "false", actual, call)
}

// NotErr return not equal error string
func NotErr(call int) string {
	err := RedBold("\n Error Trace:		" + CallerInfo()[call] + ",")
	err += Yellow("\n Error:		Equal; \n ")
	err += Blue("not expected:	'%s',\n ") + Red("but got:	'%s' \n\n")
	return err
}

// Not asserts that two objects are not equal.
//
//    tt.NotEqual(t *testing.T, 1, 1)
//
func Not(t TestingT, expect, actual interface{}, args ...int) bool {
	call := 4
	if len(args) > 0 {
		call = args[0]
	}

	expectStr := fmt.Sprint(expect)
	return NotExpect(t, expectStr, actual, call)
}

// NotEqual asserts that two objects are not equal.
//
//    tt.NotEqual(t *testing.T, 1, 1)
//
func NotEqual(t TestingT, expect, actual interface{}, args ...int) bool {
	call := 4
	if len(args) > 0 {
		call = args[0]
	}

	expectStr := fmt.Sprint(expect)
	return NotExpect(t, expectStr, actual, call)
}

// NotExpect asserts that string and objects are not equal.
//
//    tt.NotExpect(t *testing.T, "1", 1)
//
func NotExpect(t TestingT, expect string, actual interface{}, args ...int) bool {
	call := 3
	if len(args) > 0 {
		call = args[0]
	}

	actualStr := fmt.Sprint(actual)
	if expect == actualStr {
		err := NotErr(call)
		t.Errorf(err, expect, actualStr)

		return false
	}

	return true
}
